package tech

import (
	"slices"
	"ssp/set"
)

func (s *Solve) NNWing(g *Grid) (used bool, results []*Result) {
	return g.nnWing()
}

func (g *Grid) nnWing() (used bool, results []*Result) {
	alsStrongs := g.initNWingStrongs()
	for _, pivot := range g.getStableOrderedAllCells() {
		headCandStrongsss := make([][][]GroupItem, pivot.cands.Count())
		for i, cand := range pivot.cands.GetList() {
			weaks := g.findNNWeaks(cand, pivot)
			headCandStrongsss[i] = g.findNNStrongs(weaks, alsStrongs)
		}
		candHeadStrongsss := make([][][]GroupItem, 9)
		for cand := range 9 {
			candHeadStrongsss[cand] = make([][]GroupItem, len(headCandStrongsss))
		}
		for head, candStrongss := range headCandStrongsss {
			for cand, strongs := range candStrongss {
				candHeadStrongsss[cand][head] = strongs
			}
		}
		for cand, headStrongss := range candHeadStrongsss {
			if ok, rs := g.findNNZCells(pivot, cand, headStrongss); ok {
				used = true
				results = append(results, rs...)
			}
		}
	}

	return
}

func (g *Grid) seenCellsInHouse(cand int, cell *Cell, house House) (seenCells []*Cell) {
	seenCells = []*Cell{}
	for _, seenCell := range house.cells {
		if seenCell == cell {
			continue
		}
		if seenCell.cands.Has(cand) {
			seenCells = append(seenCells, seenCell)
		}
	}
	return
}

func isSameInRow(cells []*Cell, rowNum int) bool {
	for _, cell := range cells {
		if cell.rowNum != rowNum {
			return false
		}
	}
	return true
}

func isSameInCol(cells []*Cell, colNum int) bool {
	for _, cell := range cells {
		if cell.colNum != colNum {
			return false
		}
	}
	return true
}

func (g *Grid) findNNWeaks(cand int, head *Cell) (weaks []GroupItem) {
	weaks = []GroupItem{}
	rowCells := g.seenCellsInHouse(cand, head, g.rows[head.rowNum])
	for _, cells := range allCombine(rowCells) {
		weak := GroupItem{cand, [9]*Cell{}}
		for i, cell := range cells {
			weak.group[i] = cell
		}
		weaks = append(weaks, weak)
	}
	colCells := g.seenCellsInHouse(cand, head, g.cols[head.colNum])
	for _, cells := range allCombine(colCells) {
		weak := GroupItem{cand, [9]*Cell{}}
		for i, cell := range cells {
			weak.group[i] = cell
		}
		weaks = append(weaks, weak)
	}
	boxCells := g.seenCellsInHouse(cand, head, g.boxes[head.boxNum])
	for _, cells := range allCombine(boxCells) {
		//fmt.Println("cells", cells)
		if isSameInRow(cells, head.rowNum) || isSameInCol(cells, head.colNum) {
			continue
		}
		weak := GroupItem{cand, [9]*Cell{}}
		for i, cell := range cells {
			weak.group[i] = cell
		}
		weaks = append(weaks, weak)
	}

	return
}

func (g *Grid) findNNStrongs(weaks []GroupItem, alsStrongs map[GroupItem][]GroupItem) (strongss [][]GroupItem) {
	strongss = make([][]GroupItem, 9)
	for _, weak := range weaks {
		for _, strong := range alsStrongs[weak] {
			strongss[strong.cand] = append(strongss[strong.cand], strong)
		}
	}
	for i, strongs := range strongss {
		strongss[i] = mcompact(strongs)
	}
	return
}

func (g *Grid) findNNZCells(pivot *Cell, cand int, headStrongss [][]GroupItem) (used bool, results []*Result) {
	//headStrongss = slices.Clone(headStrongss)
	for i, c := range pivot.cands.GetList() {
		if c == cand {
			headStrongss[i] = []GroupItem{{cand, [9]*Cell{pivot}}}
		}
	}
	for _, zGroupItems := range mzip(headStrongss) {
		zCells := []*Cell{}
		for _, zGroupItem := range zGroupItems {
			for _, cell := range zGroupItem.getCells() {
				if cell == nil {
					break
				}
				if !cell.cands.Has(cand) {
					continue
				}
				zCells = append(zCells, cell)
			}
		}
		if len(zCells) < 2 {
			continue
		}
		ok, rmCells := g.canRemoveSeenCands(set.NewSet(cand), zCells)
		if !ok {
			continue
		}
		used = true
		if OUTPUTRESULT {
			result := new(Result)
			result.addHelpCells([]*Cell{pivot})
			for _, zGroupItem := range zGroupItems {
				cells := zGroupItem.getCells()
				if cells[0] == pivot {
					continue
				}
				result.addHelpCells(zGroupItem.getCells())
			}
			result.addRemoved(set.NewSet(cand), rmCells)
			result.setStrategyName("NNWing")
			results = append(results, result)
		}
		if CHANGE {
			removeCands(set.NewSet(cand), rmCells)
		}
		if !EXHAUSTIVE {
			return
		}
	}

	return
}

func mzip[T any](xss [][]T) [][]T {
	if len(xss) == 0 {
		return [][]T{}
	}

	result := [][]T{}
	for _, elem := range xss[0] {
		result = append(result, []T{elem})
	}

	for _, xs := range xss[1:] {
		newResult := [][]T{}
		for _, r := range result {
			for _, x := range xs {
				newCombination := append(slices.Clone(r), x)
				newResult = append(newResult, newCombination)
			}
		}
		result = newResult
	}

	return result
}

func mcontains[T comparable](xs1, xs2 []T) bool {
	for _, x := range xs2 {
		if !slices.Contains(xs1, x) {
			return false
		}
	}
	return true
}

func mcompact(xs []GroupItem) []GroupItem {
	r := []GroupItem{}
	for len(xs) > 1 {
		r = append(r, xs[0])
		ys := []GroupItem{}
		for j := 1; j < len(xs); j++ {
			if !mcontains(xs[j].getCells(), xs[0].getCells()) {
				ys = append(ys, xs[j])
			}
		}
		xs = ys
	}
	return r
}
